Skip to content

Release 1.1.0: safer defaults & bridge protocol#4

Merged
mCodex merged 4 commits intomainfrom
refactor/bumpDependencies
Apr 24, 2026
Merged

Release 1.1.0: safer defaults & bridge protocol#4
mCodex merged 4 commits intomainfrom
refactor/bumpDependencies

Conversation

@mCodex
Copy link
Copy Markdown
Owner

@mCodex mCodex commented Apr 24, 2026

Bump to v1.1.0 with several API, implementation, and tooling improvements.

Key changes:

  • Breaking API: default export removed; package exposes named exports only (improves tree-shaking).
  • Safer WebView defaults: originWhitelist now defaults to ['http://','https://'], and javaScriptEnabled is respected (disables auto-height bridge when false).
  • Introduced a namespaced bridge protocol (BRIDGE_MESSAGE_PREFIX) and MAX_COMMITTED_HEIGHT; injected bridge now posts prefixed messages and includes warm-up/guards to avoid iOS 26 layout issues.
  • useAutoHeight: stricter payload parsing, rejects out-of-range values, returns undefined until first measurement when minHeight === 0, rAF-batched commits, and honors MAX_COMMITTED_HEIGHT.
  • SizedWebView: merged safer defaults, skip bridge when JS disabled, avoid forcing container height when height is undefined, memoized component, and removed default export.
  • composeInjectedScript: improved merging and normalization (strips duplicate trailing true;, returns undefined when empty).
  • Tests: updated and extended tests to cover new defaults, JS-disabled behavior, and minHeight==0 semantics.
  • Tooling: replaced ESLint config with Biome (added biome.json), updated lefthook to run biome, updated package.json scripts/devDeps and example project deps, and added jest/node types to tsconfig.

Other: minor code cleanups and docs/README upgrade notes describing the three one-line migrations for 1.1.x.

Bump to v1.1.0 with several API, implementation, and tooling improvements.

Key changes:
- Breaking API: default export removed; package exposes named exports only (improves tree-shaking).
- Safer WebView defaults: originWhitelist now defaults to ['http://*','https://*'], and javaScriptEnabled is respected (disables auto-height bridge when false).
- Introduced a namespaced bridge protocol (BRIDGE_MESSAGE_PREFIX) and MAX_COMMITTED_HEIGHT; injected bridge now posts prefixed messages and includes warm-up/guards to avoid iOS 26 layout issues.
- useAutoHeight: stricter payload parsing, rejects out-of-range values, returns undefined until first measurement when minHeight === 0, rAF-batched commits, and honors MAX_COMMITTED_HEIGHT.
- SizedWebView: merged safer defaults, skip bridge when JS disabled, avoid forcing container height when height is undefined, memoized component, and removed default export.
- composeInjectedScript: improved merging and normalization (strips duplicate trailing `true;`, returns undefined when empty).
- Tests: updated and extended tests to cover new defaults, JS-disabled behavior, and minHeight==0 semantics.
- Tooling: replaced ESLint config with Biome (added biome.json), updated lefthook to run biome, updated package.json scripts/devDeps and example project deps, and added jest/node types to tsconfig.

Other: minor code cleanups and docs/README upgrade notes describing the three one-line migrations for 1.1.x.
Copilot AI review requested due to automatic review settings April 24, 2026 18:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Release 1.1.0 introduces safer defaults and a namespaced auto-height bridge protocol for react-native-sized-webview, along with a tooling migration (ESLint → Biome) and updated tests/docs to reflect the new behavior.

Changes:

  • Removed the default export and expanded public named exports (incl. new bridge protocol constants).
  • Introduced a namespaced postMessage protocol + max-height cap, updated useAutoHeight/SizedWebView semantics (JS-disabled mode, minHeight===0 returns undefined until first measurement).
  • Migrated lint/format tooling to Biome and updated repo/example dependencies + test expectations.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tsconfig.json Adds Jest/Node ambient types for repo typechecking.
src/utils/composeInjectedScript.ts Normalizes injected script composition and de-duplicates trailing true;.
src/index.ts Removes default export; exposes named exports only and exports new protocol constants.
src/hooks/useAutoHeight.ts Adds namespaced payload parsing, max-height cap, rAF batching, and undefined initial height when minHeight===0.
src/constants/bridgeProtocol.ts Defines shared bridge prefix and maximum committed height.
src/constants/autoHeightBridge.ts Updates injected bridge to post prefixed messages and adds iOS 26-related guards.
src/components/SizedWebView.tsx Applies safer WebView defaults, respects javaScriptEnabled, avoids forcing container height when undefined, memoizes component.
src/tests/useAutoHeight.test.tsx Adds coverage for minHeight===0 semantics and MAX cap behavior; adjusts RAF flushing.
src/tests/index.test.tsx Updates default originWhitelist expectation and adds JS-disabled/undefined-height container tests.
package.json Bumps version to 1.1.0; migrates scripts/devDeps to Biome; switches Jest preset; updates deps.
lefthook.yml Runs Biome on staged files and fixes glob pattern.
example/src/App.tsx Import order tweak.
example/package.json Updates Expo/RN/WebView deps for the example app.
example/metro.config.js Switches to node:path require.
example/babel.config.js Switches to node:path and uses arrow export.
eslint.config.mjs Removes ESLint configuration.
biome.json Adds Biome configuration for formatting/linting and overrides.
README.md Adds 1.1.0 upgrade notes plus security/bundle/tree-shaking guidance.
Comments suppressed due to low confidence (1)

src/utils/composeInjectedScript.ts:33

  • After stripping a trailing true;, a chunk can become an empty string (e.g. if the caller passes 'true;' or whitespace + 'true;'). The current loop still pushes that empty string, so parts.length becomes non-zero and the function returns "\ntrue;" instead of undefined, contradicting the doc comment about dropping empty chunks / returning undefined when nothing meaningful remains. Consider trimming after the replace and skipping the chunk if it becomes empty.
  const parts: string[] = [];

  for (const chunk of chunks) {
    if (!chunk) continue;
    // Strip an existing trailing `true;` so we always emit exactly one.
    parts.push(chunk.replace(/\s*true\s*;?\s*$/, ''));
  }

  if (parts.length === 0) {
    return undefined;
  }

  return `${parts.join('\n')}\ntrue;`;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 54 to +80
/**
* Threshold (in dp) below which height changes are ignored.
*
* Prevents excessive re-renders from minor content layout fluctuations.
* @internal
* Parses a raw payload into a positive finite pixel count, or `null` if the
* value is unusable. Accepts the namespaced protocol string, plain numbers,
* and numeric strings.
*/
const HEIGHT_DIFF_THRESHOLD = 1;
const parseHeightPayload = (rawValue: unknown): number | null => {
let candidate: unknown = rawValue;

if (
typeof candidate === 'string' &&
candidate.startsWith(BRIDGE_MESSAGE_PREFIX)
) {
candidate = candidate.slice(BRIDGE_MESSAGE_PREFIX.length);
}

const numericValue =
typeof candidate === 'number' ? candidate : Number(candidate);

if (!Number.isFinite(numericValue) || numericValue <= 0) {
return null;
}

if (numericValue > MAX_COMMITTED_HEIGHT) {
return null;
}

return Math.ceil(numericValue);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The height bridge protocol is intended to be namespaced, but parseHeightPayload will currently accept any numeric string (e.g. '360') because it falls back to Number(candidate) even when the string has no BRIDGE_MESSAGE_PREFIX. That means any page running inside the WebView can still mutate the container height by posting a plain numeric string, undermining the security guarantee and the docs in this PR. Consider requiring the prefix for string payloads (while still allowing number payloads for direct/manual calls/tests), and update the tests that currently send '240' / '360' accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines 91 to 96
const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
setHeightFromPayload(event.nativeEvent.data);
if (isJsEnabled) {
setHeightFromPayload(event.nativeEvent.data);
}
onMessage?.(event);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleMessage currently forwards every onMessage payload into setHeightFromPayload (when JS is enabled). If setHeightFromPayload continues to accept bare numeric strings, any user-land postMessage('123') can resize the container. To make the namespaced protocol effective, gate the dispatch on the prefix (e.g. only call setHeightFromPayload when event.nativeEvent.data is a string starting with BRIDGE_MESSAGE_PREFIX) and always forward all messages to the consumer's onMessage unchanged.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +15
* Messages without this prefix are treated as user-land traffic and forwarded
* untouched to the consumer's `onMessage` handler. This prevents hostile or
* unrelated pages from being able to mutate the container height just by
* sending a numeric string through `window.ReactNativeWebView.postMessage`.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment claims that messages without BRIDGE_MESSAGE_PREFIX can't mutate the container height, but the current hook implementation still accepts numeric strings without the prefix (and SizedWebView feeds all messages to the hook). Either enforce the prefix requirement in code, or soften this comment to avoid giving a false sense of security.

Suggested change
* Messages without this prefix are treated as user-land traffic and forwarded
* untouched to the consumer's `onMessage` handler. This prevents hostile or
* unrelated pages from being able to mutate the container height just by
* sending a numeric string through `window.ReactNativeWebView.postMessage`.
* This marks bridge-originated traffic so the hook/component can distinguish
* it from other `window.ReactNativeWebView.postMessage` payloads when applying
* the shared protocol. It is a protocol identifier, not by itself a guarantee
* that unprefixed messages will be ignored by every consumer path.

Copilot uses AI. Check for mistakes.
Comment thread README.md

## 🛡️ Security

- **Namespaced message protocol.** The injected bridge posts values prefixed with `__RN_SIZED_WV__:` and the hook rejects everything else, so your own `onMessage` traffic cannot accidentally (or maliciously) mutate the container height.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README security section says the hook "rejects everything else" besides prefixed bridge messages, but the current useAutoHeight implementation still accepts bare numeric strings (and SizedWebView passes all onMessage payloads to it). This makes the namespacing claim inaccurate. Either enforce prefix-only handling for string payloads (recommended) or adjust the README wording to match actual behavior.

Suggested change
- **Namespaced message protocol.** The injected bridge posts values prefixed with `__RN_SIZED_WV__:` and the hook rejects everything else, so your own `onMessage` traffic cannot accidentally (or maliciously) mutate the container height.
- **Namespaced message protocol.** The injected bridge posts values prefixed with `__RN_SIZED_WV__:` so bridge traffic is clearly namespaced. For backward compatibility, the auto-height hook also accepts bare numeric height strings, so custom `onMessage` handlers should avoid sending raw numeric payloads unless they are intended to affect sizing.

Copilot uses AI. Check for mistakes.
mCodex and others added 3 commits April 24, 2026 15:16
Only accept the bridge namespaced payloads for height updates and forward other messages to consumers. SizedWebView now checks that JS messages are strings starting with the bridge prefix before calling setHeightFromPayload; unprefixed payloads are passed to the caller's onMessage. useAutoHeight.parseHeightPayload was tightened to accept only numbers or prefixed bridge strings (bare numeric strings are rejected).

The injected AUTO_HEIGHT_BRIDGE script was optimized: added a domDirty flag (set by the MutationObserver and cleared after pruning) so pruneTrailingNodes runs only when structure changed; prefer the wrapper element as the single authoritative reflow target to avoid multiple forced layouts; and other small measurement-path simplifications to reduce reflows and work per frame.

Documentation: README gains a “Built for speed” section describing hot-path complexity and rationale for the optimizations.
Extend useAutoHeight tests to cover invalid payload cases: a prefixed but non-numeric payload ('__RN_SIZED_WV__:not-a-number') to exercise the isFinite branch, and a direct negative number (-10) to exercise the numericValue <= 0 branch. Both assertions verify that requestAnimationFrame is not called and the height remains unchanged, preventing regressions for malformed or out-of-range payloads.
Co-authored-by: Copilot <copilot@github.com>
@mCodex mCodex merged commit ee9fe39 into main Apr 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants